package com.sprouting.ops.service;

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.sprouting.ops.dao.DromaraFileStorageDao;
import com.sprouting.ops.exception.ServiceException;
import com.sprouting.ops.interfaces.ConstantBase;
import com.sprouting.ops.model.DromaraFileStorage;
import com.sprouting.ops.model.qo.DromaraFileStorageQo;
import com.sprouting.ops.model.vo.DromaraFileStorageVo;
import com.sprouting.ops.utils.CheckTool;
import com.sprouting.ops.utils.EntityTool;
import com.sprouting.ops.utils.IdTool;
import lombok.extern.slf4j.Slf4j;
import org.dromara.x.file.storage.core.FileInfo;
import org.dromara.x.file.storage.core.hash.HashInfo;
import org.dromara.x.file.storage.core.recorder.FileRecorder;
import org.dromara.x.file.storage.core.upload.FilePartInfo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 统一文件存储记录信息
 * 用来将文件上传记录保存到数据库
 * 接口文档：https://x-file-storage.xuyanwu.cn/#/%E5%9F%BA%E7%A1%80%E5%8A%9F%E8%83%BD?id=%e4%bf%9d%e5%ad%98%e4%b8%8a%e4%bc%a0%e8%ae%b0%e5%bd%95
 *
 * @author ：LX（长沙麓谷）
 * @date 创建时间： 2025/2/10 17:49
 */
@Service
@Slf4j
public class DromaraFileStorageService extends ServiceImpl<DromaraFileStorageDao, DromaraFileStorage> implements FileRecorder {

    @Resource
    private DromaraFileStorageDao dromaraFileStorageDao;
    @Resource
    private DromaraPartDetailService dromaraPartDetailService;

    /**
     * 临时文件在数据库保存的时间，单位天，比如一个文件2天前上传，这里配置了2，则2天后发现状态还没修改为已使用，则删除
     */
    @Value("${bud.customize.storage.temp-del-time:2}")
    private Integer tempDelTime;

    /**
     * 匹配 ![alt text](url) 格式，提取的只有url，老版本用
     */
    // private Pattern pattern = Pattern.compile("\\!\\[.*?\\]\\((.*?)\\)");
    /**
     * 匹配 ![alt text](url) 格式，并提取 id 参数值
     */
    private Pattern pattern = Pattern.compile("!\\[.*?]\\((?:.*?[?&]id=)(\\d+)\\)");

    /**
     * 分页查询文件列表
     * @param page 分页参数
     * @param query 查询参数
     * @return
     */
    public Page<DromaraFileStorageVo> getFileRecordInfoPage(Page page, DromaraFileStorageQo query) {
        Page<DromaraFileStorageVo> listPage = dromaraFileStorageDao.getFileRecordInfoPage(page, query);
        List<DromaraFileStorageVo> records = listPage.getRecords();
        if (CollUtil.isEmpty(records)){
            return listPage;
        }
        for (DromaraFileStorageVo record : records) {
            record.setSizeCh(FileUtil.readableFileSize(record.getSize()));
        }

        return listPage;
    }

    /**
     * 获取可清理的文件
     * @param now 当前时间
     * @return
     */
    public List<DromaraFileStorage> getCleanFile(Date now) {
        return dromaraFileStorageDao.getCleanFile(now);
    }

    /**
     * 根据绑定ID查询
     * @param objectId 绑定id
     * @return
     */
    public List<DromaraFileStorage> getByBindId(String objectId) {
        return dromaraFileStorageDao.getByBindId(objectId);
    }

    /**
     * 提取文档中的Markdown的图片信息并转换为生产文件对象
     * @param text 要提取的文本，提取的数据内容是 ![xx](xxx) 中的 （xxx）中的数据
     * @param objectId 绑定id
     * @param objectType 绑定类型 {@link ConstantBase.FileSourceType}
     * @return
     */
    public List<DromaraFileStorage> getMarkdownImagesUrlToFileStorage(String text, String objectId, String objectType){
        // List<String> urlList = getMarkdownImagesUrl(text);
        // 获取md的id
        List<String> urlList = getMarkdownImagesId(text);

        List<DromaraFileStorage> dromaraFileStorageList = new ArrayList<>();
        if (CollUtil.isNotEmpty(urlList)){
            for (String url : urlList) {
                // 需要将这些数据转换为正式的
                DromaraFileStorage fileStorageList = toProductionByUrlBefore(url, objectId, objectType);
                dromaraFileStorageList.add(fileStorageList);
            }
        }

        return dromaraFileStorageList;
    }

    /**
     * 提取文档中的Markdown的图片信息
     * @param text 要提取的文本，提取的数据内容是 ![xx](xxx) 中的 （xxx）中的数据,获取的url，就是xxx中的数据
     * @return
     */
    public List<String> getMarkdownImagesUrl(String text){
        List<String> urlList = new ArrayList<>();
        if (StrUtil.isNotEmpty(text)){
            Matcher matcher = pattern.matcher(text);
            while (matcher.find()) {
                // 提取括号内的内容, ![xxx](xxx) 提取 (xxx) 中的数据
                String url = matcher.group(1);
                urlList.add(url);
            }
        }

        return urlList;
    }

    /**
     * 提取文档中的Markdown的图片信息
     * @param text 要提取的文本，提取的数据内容是 ![xx](xxx?id=123) 中的 （123）中的数据,id的值
     * @return
     */
    public List<String> getMarkdownImagesId(String text){
        List<String> idList = new ArrayList<>();
        if (StrUtil.isNotEmpty(text)){
            Matcher matcher = pattern.matcher(text);
            while (matcher.find()) {
                // 提取括号内的内容, ![xxx](xxx) 提取 (xxx) 中的数据
                String id = matcher.group(1);
                idList.add(id);
            }
        }

        return idList;
    }

    /*
     * =======================常用的公共方法 start===========================
     * =======================常用的公共方法 start===========================
     * =======================常用的公共方法 start===========================
     */

    /**
     * 临时文件转为生产(基于url的)，注意，还没保存（保存应该有事务控制，和objectId所在表一起入库）(还未真正的删除)
     * @param id 上传的文件url，就是 {@link DromaraFileStorage#getId()} } }
     * @param objectId 绑定id，所属表的ID 。这个参数根据外部情况传入，一般是这个对应的表ID
     * @param objectType 所属的类型，表 {@link com.sprouting.ops.interfaces.ConstantBase.FileSourceType}
     * @return 返回这个处理后的临时文件记录
     */
    public DromaraFileStorage toProductionByUrlBefore(String id, String objectId, String objectType){
        if (CheckTool.checkNull(id)){
            return null;
        }

        // LambdaQueryWrapper<DromaraFileStorage> qw = new LambdaQueryWrapper<DromaraFileStorage>().eq(DromaraFileStorage::getUrl, url);
        // DromaraFileStorage dromaraFileStorage = dromaraFileStorageDao.selectOne(qw);
        DromaraFileStorage dromaraFileStorage = dromaraFileStorageDao.selectById(id);
        if (dromaraFileStorage == null){
            throw new ServiceException("无效的上传文件");
        }

        dromaraFileStorage.setTempFile(ConstantBase.TempFile.PRODUCTION);
        // 给定一个未来的时间
        dromaraFileStorage.setDelTime(DateUtil.parseDateTime("9999-01-01 00:00:00"));
        // 一些绑定参数
        dromaraFileStorage.setObjectId(objectId);
        dromaraFileStorage.setObjectType(objectType);
        return dromaraFileStorage;
    }

    /**
     * 临时文件转为生产，注意，还没保存（保存应该有事务控制，和objectId所在表一起入库）(还未真正的删除)
     * @param uploadFileId 上传的文件id，就是 {@link DromaraFileStorage#getId()} 的id，一般上传成功后这个参数会返回
     * @param objectId 绑定id，所属表的ID 。这个参数根据外部情况传入，一般是这个对应的表ID
     * @param objectType 所属的类型，表 {@link com.sprouting.ops.interfaces.ConstantBase.FileSourceType}
     * @return 返回这个处理后的临时文件记录
     */
    public DromaraFileStorage toProductionBefore(String uploadFileId, String objectId, String objectType){
        if (CheckTool.checkNull(uploadFileId)){
            return null;
        }

        DromaraFileStorage dromaraFileStorage = dromaraFileStorageDao.selectById(uploadFileId);
        if (dromaraFileStorage == null){
            throw new ServiceException("无效的上传文件");
        }

        dromaraFileStorage.setTempFile(ConstantBase.TempFile.PRODUCTION);
        // 给定一个未来的时间
        dromaraFileStorage.setDelTime(DateUtil.parseDateTime("9999-01-01 00:00:00"));
        // 一些绑定参数
        dromaraFileStorage.setObjectId(objectId);
        dromaraFileStorage.setObjectType(objectType);
        return dromaraFileStorage;
    }

    /**
     * 更新文件的情况(还未真正的更新)
     * @param oldUploadFileId 之前上传的文件id，可能为空  {@link DromaraFileStorage#getId()}
     * @param newUploadFileId 新上传的文件id， {@link DromaraFileStorage#getId()}
     * @param objectId 绑定id，所属表的ID 。这个参数根据外部情况传入，一般是这个对应的表ID 。除非确定没有上传新的文件，否则外部都应该传入
     * @param objectType 所属的类型，表 {@link com.sprouting.ops.interfaces.ConstantBase.FileSourceType} 。除非确定没有上传新的文件，否则外部都应该传入
     * @return 返回一个数组，数组下标0， 新的文件  数组下标1 旧的文件，一般会被删除
     */
    public DromaraFileStorage[] updFileBefore(String newUploadFileId, String oldUploadFileId, String objectId, String objectType){
        if (CheckTool.checkNull(newUploadFileId)){
            // 现在的没有，检查旧的有没有
            if (CheckTool.checkNull(oldUploadFileId)){
                // 两个都没有，则不做任何处理。场景：说明2次都没上传文件
                return new DromaraFileStorage[2];

            } else {
                // 之前的有，则要删除之前的。场景：新的删除了文件，旧的有文件
                DromaraFileStorage opsUploadFile = dromaraFileStorageDao.selectById(oldUploadFileId);
                if (opsUploadFile != null){
                    opsUploadFile.setDelTime(new Date());
                    opsUploadFile.setTempFile(ConstantBase.TempFile.PRODUCTION_DEL);

                    DromaraFileStorage[] uploadFileArray = new DromaraFileStorage[2];
                    uploadFileArray[0] = null;
                    uploadFileArray[1] = opsUploadFile;
                    return uploadFileArray;
                }

                // 新旧都为空
                return new DromaraFileStorage[2];
            }

        } else {
            // 现在的有，检查之前的有没有
            if (CheckTool.checkNull(oldUploadFileId)){
                // 之前的没有，但新的有。进行保存操作
                DromaraFileStorage[] uploadFileArray = new DromaraFileStorage[2];
                uploadFileArray[0] = toProductionBefore(newUploadFileId, objectId, objectType);
                uploadFileArray[1] = null;
                return uploadFileArray;

            } else {
                // 新的和旧的都有，检查是否同一个
                if (newUploadFileId.equals(oldUploadFileId)){
                    // 新旧都有，但属于同一个文件，不做处理。
                    return new DromaraFileStorage[2];

                } else {
                    // 上传了新的图片。需要将新的设置为正式，旧的设置为删除
                    DromaraFileStorage[] uploadFileArray = new DromaraFileStorage[2];
                    uploadFileArray[0] = toProductionBefore(newUploadFileId, objectId, objectType);
                    uploadFileArray[1] = delFileBefore(oldUploadFileId);
                    return uploadFileArray;
                }
            }
        }
    }

    /**
     * 删除文件(还未真正的删除)
     * 需要考虑一点，因为我们都是逻辑删除，这种是否必要真的删除，但现在的直接删
     * @param uploadFileId 文件ID
     * @return 如果返回非null，则存在文件，返回的文件更新后就会被定时器删除
     */
    public DromaraFileStorage delFileBefore(String uploadFileId){
        if (CheckTool.checkNull(uploadFileId)){
            return null;
        }

        DromaraFileStorage opsUploadFile = dromaraFileStorageDao.selectById(uploadFileId);
        if (opsUploadFile != null){
            if (ConstantBase.TempFile.TEMP.equals(opsUploadFile.getTempFile())){
                // 还是临时文件，临时文件则不做操作
            } else {
                // 已经不是临时文件了
                opsUploadFile.setTempFile(ConstantBase.TempFile.PRODUCTION_DEL);
            }
            opsUploadFile.setDelTime(new Date());
            return opsUploadFile;
        }

        return null;
    }


    /*
     * =======================常用的公共方法 end===========================
     * =======================常用的公共方法 end===========================
     * =======================常用的公共方法 end===========================
     */





    /*
    ========================= 官方需要实现的接口 start ============================
    ========================= 官方需要实现的接口 start ============================
    ========================= 官方需要实现的接口 start ============================
     */

    /**
     * 保存文件信息到数据库
     * 现在的文件保存不需要手动去调用保存接口了，也会自动生成文件id
     * @param fileInfo 框架自动生成的数据
     * @return
     */
    @Override
    public boolean save(FileInfo fileInfo) {
        DromaraFileStorage detail = toFileDetail(fileInfo);

        // 额外的参数，可以使用 fileInfo.getAttr() 来获取
        // 自定义的一些数据，用来填充扩展信息,这一块非官方代码，自己根据需求扩展  --- start
        detail.setId(IdTool.getId());
        Dict attr = fileInfo.getAttr();
        if (attr != null){
            String userId = attr.getStr(EntityTool.getFieldName(DromaraFileStorage::getUserId));
            if (StrUtil.isNotEmpty(userId)){
                detail.setUserId(userId);
            }
            Date delTime = attr.getDate(EntityTool.getFieldName(DromaraFileStorage::getDelTime));
            if (delTime != null){
                detail.setDelTime(delTime);
            }
            Integer tempFile = attr.getInt(EntityTool.getFieldName(DromaraFileStorage::getTempFile));
            if (tempFile != null){
                detail.setTempFile(tempFile);
            }
        }
        if (StrUtil.isEmpty(detail.getUserId())){
            detail.setUserId(StpUtil.getLoginIdAsString());
        }
        if (detail.getDelTime() == null){
            detail.setDelTime(DateUtil.offsetDay(new Date(), tempDelTime));
        }
        if (detail.getTempFile() == null){
            detail.setTempFile(ConstantBase.TempFile.TEMP);
        }
        // 自定义结束 --- end

        log.info(detail.getId() + "上传文件保存成功~");
        dromaraFileStorageDao.insert(detail);
        fileInfo.setId(detail.getId());
        return true;
    }

    /**
     * 更新文件记录，可以根据文件 ID 或 URL 来更新文件记录，
     * 主要用在手动分片上传文件-完成上传，作用是更新文件信息
     * @param fileInfo 框架自动生成的数据
     */
    @Override
    public void update(FileInfo fileInfo) {
        DromaraFileStorage detail = toFileDetail(fileInfo);

        LambdaQueryWrapper<DromaraFileStorage> qw = new LambdaQueryWrapper<DromaraFileStorage>()
                .eq(detail.getUrl() != null, DromaraFileStorage::getUrl, detail.getUrl())
                .eq(detail.getId() != null, DromaraFileStorage::getId, detail.getId());
        dromaraFileStorageDao.update(detail, qw);
    }

    /**
     * 根据 url 查询文件信息
     */
    @Override
    public FileInfo getByUrl(String url) {
        LambdaQueryWrapper<DromaraFileStorage> qw = new LambdaQueryWrapper<DromaraFileStorage>().eq(DromaraFileStorage::getUrl, url);
        DromaraFileStorage dromaraFileStorage = dromaraFileStorageDao.selectOne(qw);
        return toFileInfo(dromaraFileStorage);
    }

    /**
     * 根据 url 删除文件信息
     */
    @Override
    public boolean delete(String url) {
        LambdaQueryWrapper<DromaraFileStorage> qw = new LambdaQueryWrapper<DromaraFileStorage>().eq(DromaraFileStorage::getUrl, url);
        dromaraFileStorageDao.delete(qw);
        return true;
    }

    /**
     * 保存文件分片信息
     * @param filePartInfo 文件分片信息
     */
    @Override
    public void saveFilePart(FilePartInfo filePartInfo) {
        dromaraPartDetailService.saveFilePart(filePartInfo);
    }

    /**
     * 删除文件分片信息
     */
    @Override
    public void deleteFilePartByUploadId(String uploadId) {
        dromaraPartDetailService.deleteFilePartByUploadId(uploadId);
    }

    /*
    ========================= 官方需要实现的接口 end ============================
    ========================= 官方需要实现的接口 end ============================
    ========================= 官方需要实现的接口 end ============================
     */


     /*
    ========================= 官方私有方法 start ============================
    ========================= 官方私有方法 start ============================
    ========================= 官方私有方法 start ============================
     */

    /**
     * 将 FileInfo 转为 存储到数据库的对象
     */
    public DromaraFileStorage toFileDetail(FileInfo info){
        DromaraFileStorage detail = BeanUtil.copyProperties(info, DromaraFileStorage.class, "metadata", "userMetadata", "thMetadata", "thUserMetadata", "attr", "hashInfo");

        // 这里手动获 元数据 并转成 json 字符串，方便存储在数据库中
        detail.setMetadata(valueToJson(info.getMetadata()));
        detail.setUserMetadata(valueToJson(info.getUserMetadata()));
        detail.setThMetadata(valueToJson(info.getThMetadata()));
        detail.setThUserMetadata(valueToJson(info.getThUserMetadata()));
        // 这里手动获 取附加属性字典 并转成 json 字符串，方便存储在数据库中
        detail.setAttr(valueToJson(info.getAttr()));
        // 这里手动获 哈希信息 并转成 json 字符串，方便存储在数据库中
        detail.setHashInfo(valueToJson(info.getHashInfo()));
        return detail;
    }

    /**
     * 将 FileDetail 转为 FileInfo
     */
    public FileInfo toFileInfo(DromaraFileStorage detail){
        if (detail == null){
            return null;
        }
        FileInfo info = BeanUtil.copyProperties(
                detail, FileInfo.class, "metadata", "userMetadata", "thMetadata", "thUserMetadata", "attr", "hashInfo");

        // 这里手动获取数据库中的 json 字符串 并转成 元数据，方便使用
        info.setMetadata(jsonToMetadata(detail.getMetadata()));
        info.setUserMetadata(jsonToMetadata(detail.getUserMetadata()));
        info.setThMetadata(jsonToMetadata(detail.getThMetadata()));
        info.setThUserMetadata(jsonToMetadata(detail.getThUserMetadata()));
        // 这里手动获取数据库中的 json 字符串 并转成 附加属性字典，方便使用
        info.setAttr(jsonToDict(detail.getAttr()));
        // 这里手动获取数据库中的 json 字符串 并转成 哈希信息，方便使用
        info.setHashInfo(jsonToHashInfo(detail.getHashInfo()));
        return info;
    }

    /**
     * 将指定值转换成 json 字符串
     */
    private String valueToJson(Object value){
        if (value == null) {
            return null;
        }

        return JSONUtil.toJsonStr(value);
    }

    /**
     * 将 json 字符串转换成元数据对象
     */
    private Map<String, String> jsonToMetadata(String json) {
        if (StrUtil.isBlank(json)) {
            return null;
        }

        return JSONUtil.toBean(json, new TypeReference<Map<String, String>>() {}, true);
    }

    /**
     * 将 json 字符串转换成字典对象
     */
    private Dict jsonToDict(String json){
        if (StrUtil.isBlank(json)) {
            return null;
        }
        return JSONUtil.toBean(json, Dict.class);
    }

    /**
     * 将 json 字符串转换成哈希信息对象
     */
    public HashInfo jsonToHashInfo(String json) {
        if (StrUtil.isBlank(json)) {
            return null;
        }
        return JSONUtil.toBean(json, HashInfo.class);
    }




    /*
    ========================= 官方私有方法 end ============================
    ========================= 官方私有方法 end ============================
    ========================= 官方私有方法 end ============================
     */
}
